package com.xomily.web.core.config;

import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * XSS攻击过滤器
 * <p>
 * 此过滤器用于清理传入请求和响应中的跨站脚本（XSS）攻击它通过包装HttpServletRequest和HttpServletResponse对象，
 * 并在访问其属性和方法时应用XSS清理此过滤器还支持通过配置排除特定的URL，以避免对某些资源进行不必要的过滤
 */
public class XSSNewFilter implements Filter {

    // 默认的排除URL模式
    private static final String DEFAULT_EXCLUDES = "/captchaImage/,/css/*,/js/*,/images/*,/favicon.ico";

    // 存储需要排除过滤的URL模式
    private List<String> excludes = new ArrayList<>();

    /**
     * 初始化过滤器
     * <p>
     * 此方法从过滤器配置中获取排除URL的初始化参数，并将其转换为字符串数组存储在excludes列表中如果未提供初始化参数，
     * 则使用默认的排除URL模式
     *
     * @param filterConfig 过滤器配置对象，用于获取初始化参数
     * @throws ServletException 如果初始化过程中发生错误
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String tempExcludes = filterConfig.getInitParameter("excludes");
        if (StringUtils.isNotEmpty(tempExcludes)) {
            String[] url = tempExcludes.split(",");
            Collections.addAll(excludes, url);
        } else {
            Collections.addAll(excludes, DEFAULT_EXCLUDES.split(","));
        }
        System.err.println("XSSNewFilter 已初始化，排除的 URL：" + excludes);
    }

    /**
     * 执行过滤操作
     * <p>
     * 此方法用于过滤传入的请求和响应，以防止跨站脚本（XSS）攻击它首先检查是否应排除请求的URL，
     * 如果是，则直接继续处理链中的下一个过滤器或Servlet如果不是排除的URL，则创建包装了请求和响应的
     * 对象，这些对象在处理时会应用XSS清理，然后将控制权传递给链中的下一个元素
     *
     * @param request  Servlet请求对象，用于获取请求数据
     * @param response Servlet响应对象，用于向客户端发送响应数据
     * @param chain    过滤链对象，用于将请求和响应传递给链中的下一个过滤器或资源
     * @throws IOException      如果在执行过滤过程中发生I/O错误
     * @throws ServletException 如果在执行过滤过程中发生Servlet异常
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 将通用ServletRequest和ServletResponse转换为HTTP特定类型
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        System.err.println("正在处理请求：" + req.getRequestURI());

        // 检查是否应排除当前请求的URL
        if (handleExcludeURL(req)) {
            System.err.println("排除 URL：" + req.getRequestURI());
            // 如果是排除的URL，直接传递原始请求和响应，不进行XSS处理
            chain.doFilter(request, response);
            return;
        }

        // 创建包装请求和响应对象
        // 这些对象将在处理时自动清理XSS攻击
        XSSHttpServletRequestWrapper xssRequest = new XSSHttpServletRequestWrapper(req);
        XSSHttpServletResponseWrapper xssResponse = new XSSHttpServletResponseWrapper(resp);

        // 将包装对象转换为父接口类型以确保兼容性
        // 继续处理链中的下一个过滤器或Servlet，传递清理后的请求和响应对象
        chain.doFilter(xssRequest, xssResponse);
    }

    /**
     * 检查是否应排除当前请求的URL
     * <p>
     * 此方法检查当前请求的URL是否匹配任何预定义的排除模式如果匹配，则返回true，表示应排除此请求的过滤
     * 否则，返回false，表示应应用XSS清理
     *
     * @param request HTTP请求对象，用于获取请求的URL路径
     * @return 如果应排除当前请求的URL，则返回true；否则返回false
     */
    private boolean handleExcludeURL(HttpServletRequest request) {
        String url = request.getServletPath();
        String method = request.getMethod();

        // GET 和 DELETE 请求不进行过滤
//        if (method == null || method.matches("GET") || method.matches("DELETE")) {
//            System.err.println("排除方法：" + method);
//            return true;
//        }

        boolean excluded = excludes.stream().anyMatch(url::matches);
        if (excluded) {
            System.err.println("排除 URL：" + url);
        }
        return excluded;
    }

    @Override
    public void destroy() {
        System.err.println("XSSNewFilter 已销毁");
    }

    /**
     * 包装HttpServletRequest以进行XSS清理
     * <p>
     * 此类通过重写HttpServletRequest的方法来清理请求参数、头信息和查询字符串中的XSS攻击它还处理请求体的过滤
     * 对于传入的每个值，都会应用XSS清理
     */
    public static class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {

        public XSSHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public String[] getParameterValues(String name) {
            String[] values = super.getParameterValues(name);
            if (values != null) {
                String[] cleanedValues = Arrays.stream(values)
                        .map(this::clean)
                        .toArray(String[]::new);
                System.err.println("已清理参数值 '" + name + "'：" + Arrays.toString(cleanedValues));
                return cleanedValues;
            }
            return super.getParameterValues(name);
        }

        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            String cleanedValue = value != null ? clean(value) : null;
            System.err.println("已清理参数 '" + name + "'：" + cleanedValue);
            return cleanedValue;
        }

        @Override
        public String getHeader(String name) {
            String value = super.getHeader(name);
            String cleanedValue = value != null ? clean(value) : null;
            System.err.println("已清理请求头 '" + name + "'：" + cleanedValue);
            return cleanedValue;
        }

        @Override
        public String getQueryString() {
            String query = super.getQueryString();
            String cleanedQuery = query != null ? clean(query) : null;
            System.err.println("已清理查询字符串：" + cleanedQuery);
            return cleanedQuery;
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            String body = IOUtils.toString(super.getInputStream(), StandardCharsets.UTF_8);
            if (body != null) {
                String cleanedBody = clean(body);
                System.err.println("已清理请求体：" + cleanedBody);
                return new ServletInputStreamWrapper(cleanedBody.getBytes(StandardCharsets.UTF_8));
            }
            return super.getInputStream();
        }

        /**
         * 清理XSS攻击的方法
         * <p>
         * 此方法使用Apache Commons Text库来清理HTML输入，防止XSS攻击如果输入为null，则返回null
         *
         * @param input 待清理的输入字符串
         * @return 清理后的字符串，如果输入为null，则返回null
         */
        private String clean(String input) {
            if (input == null) {
                return null;
            }
            return StringEscapeUtils.escapeHtml4(input);
        }

        /**
         * 包装ServletInputStream以处理请求体
         * <p>
         * 此类通过重写ServletInputStream的方法来提供对经过XSS清理的请求体的访问
         */
        private static class ServletInputStreamWrapper extends ServletInputStream {
            private final ByteArrayInputStream byteArrayInputStream;

            public ServletInputStreamWrapper(byte[] bytes) {
                this.byteArrayInputStream = new ByteArrayInputStream(bytes);
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        }
    }

    /**
     * 包装HttpServletResponse以进行XSS清理
     * <p>
     * 此类通过重写HttpServletResponse的方法来清理发送给客户端的错误消息和重定向URL中的XSS攻击
     */
    public static class XSSHttpServletResponseWrapper extends HttpServletResponseWrapper {

        public XSSHttpServletResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        @Override
        public void sendError(int sc, String msg) throws IOException {
            if (msg != null) {
                msg = StringEscapeUtils.escapeHtml4(msg);
                System.err.println("已清理错误消息：" + msg);
            }
            super.sendError(sc, msg);
        }

        @Override
        public void sendRedirect(String location) throws IOException {
            if (location != null) {
                location = StringEscapeUtils.escapeHtml4(location);
                System.err.println("已清理重定向位置：" + location);
            }
            super.sendRedirect(location);
        }
    }
}
